// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

(function(mod) {
  if (typeof exports == 'object' && typeof module == 'object')
    // CommonJS
    mod(require('../../lib/codemirror'));
  else if (typeof define == 'function' && define.amd)
    // AMD
    define(['../../lib/codemirror'], mod);
  // Plain browser env
  else mod(CodeMirror);
})(function(CodeMirror) {
  'use strict';

  function forEach(arr, f) {
    for (var i = 0; i < arr.length; i++) f(arr[i], i);
  }
  function some(arr, f) {
    for (var i = 0; i < arr.length; i++) if (f(arr[i], i)) return true;
    return false;
  }

  CodeMirror.defineMode('dylan', function(_config) {
    // Words
    var words = {
      // Words that introduce unnamed definitions like "define interface"
      unnamedDefinition: ['interface'],

      // Words that introduce simple named definitions like "define library"
      namedDefinition: [
        'module',
        'library',
        'macro',
        'C-struct',
        'C-union',
        'C-function',
        'C-callable-wrapper',
      ],

      // Words that introduce type definitions like "define class".
      // These are also parameterized like "define method" and are
      // appended to otherParameterizedDefinitionWords
      typeParameterizedDefinition: ['class', 'C-subtype', 'C-mapped-subtype'],

      // Words that introduce trickier definitions like "define method".
      // These require special definitions to be added to startExpressions
      otherParameterizedDefinition: [
        'method',
        'function',
        'C-variable',
        'C-address',
      ],

      // Words that introduce module constant definitions.
      // These must also be simple definitions and are
      // appended to otherSimpleDefinitionWords
      constantSimpleDefinition: ['constant'],

      // Words that introduce module variable definitions.
      // These must also be simple definitions and are
      // appended to otherSimpleDefinitionWords
      variableSimpleDefinition: ['variable'],

      // Other words that introduce simple definitions
      // (without implicit bodies).
      otherSimpleDefinition: ['generic', 'domain', 'C-pointer-type', 'table'],

      // Words that begin statements with implicit bodies.
      statement: [
        'if',
        'block',
        'begin',
        'method',
        'case',
        'for',
        'select',
        'when',
        'unless',
        'until',
        'while',
        'iterate',
        'profiling',
        'dynamic-bind',
      ],

      // Patterns that act as separators in compound statements.
      // This may include any general pattern that must be indented
      // specially.
      separator: [
        'finally',
        'exception',
        'cleanup',
        'else',
        'elseif',
        'afterwards',
      ],

      // Keywords that do not require special indentation handling,
      // but which should be highlighted
      other: [
        'above',
        'below',
        'by',
        'from',
        'handler',
        'in',
        'instance',
        'let',
        'local',
        'otherwise',
        'slot',
        'subclass',
        'then',
        'to',
        'keyed-by',
        'virtual',
      ],

      // Condition signaling function calls
      signalingCalls: [
        'signal',
        'error',
        'cerror',
        'break',
        'check-type',
        'abort',
      ],
    };

    words['otherDefinition'] = words['unnamedDefinition']
      .concat(words['namedDefinition'])
      .concat(words['otherParameterizedDefinition']);

    words['definition'] = words['typeParameterizedDefinition'].concat(
      words['otherDefinition'],
    );

    words['parameterizedDefinition'] = words[
      'typeParameterizedDefinition'
    ].concat(words['otherParameterizedDefinition']);

    words['simpleDefinition'] = words['constantSimpleDefinition']
      .concat(words['variableSimpleDefinition'])
      .concat(words['otherSimpleDefinition']);

    words['keyword'] = words['statement']
      .concat(words['separator'])
      .concat(words['other']);

    // Patterns
    var symbolPattern = '[-_a-zA-Z?!*@<>$%]+';
    var symbol = new RegExp('^' + symbolPattern);
    var patterns = {
      // Symbols with special syntax
      symbolKeyword: symbolPattern + ':',
      symbolClass: '<' + symbolPattern + '>',
      symbolGlobal: '\\*' + symbolPattern + '\\*',
      symbolConstant: '\\$' + symbolPattern,
    };
    var patternStyles = {
      symbolKeyword: 'atom',
      symbolClass: 'tag',
      symbolGlobal: 'variable-2',
      symbolConstant: 'variable-3',
    };

    // Compile all patterns to regular expressions
    for (var patternName in patterns)
      if (patterns.hasOwnProperty(patternName))
        patterns[patternName] = new RegExp('^' + patterns[patternName]);

    // Names beginning "with-" and "without-" are commonly
    // used as statement macro
    patterns['keyword'] = [/^with(?:out)?-[-_a-zA-Z?!*@<>$%]+/];

    var styles = {};
    styles['keyword'] = 'keyword';
    styles['definition'] = 'def';
    styles['simpleDefinition'] = 'def';
    styles['signalingCalls'] = 'builtin';

    // protected words lookup table
    var wordLookup = {};
    var styleLookup = {};

    forEach(
      ['keyword', 'definition', 'simpleDefinition', 'signalingCalls'],
      function(type) {
        forEach(words[type], function(word) {
          wordLookup[word] = type;
          styleLookup[word] = styles[type];
        });
      },
    );

    function chain(stream, state, f) {
      state.tokenize = f;
      return f(stream, state);
    }

    function tokenBase(stream, state) {
      // String
      var ch = stream.peek();
      if (ch == "'" || ch == '"') {
        stream.next();
        return chain(stream, state, tokenString(ch, 'string'));
      }
      // Comment
      else if (ch == '/') {
        stream.next();
        if (stream.eat('*')) {
          return chain(stream, state, tokenComment);
        } else if (stream.eat('/')) {
          stream.skipToEnd();
          return 'comment';
        }
        stream.backUp(1);
      }
      // Decimal
      else if (/[+\-\d\.]/.test(ch)) {
        if (
          stream.match(/^[+-]?[0-9]*\.[0-9]*([esdx][+-]?[0-9]+)?/i) ||
          stream.match(/^[+-]?[0-9]+([esdx][+-]?[0-9]+)/i) ||
          stream.match(/^[+-]?\d+/)
        ) {
          return 'number';
        }
      }
      // Hash
      else if (ch == '#') {
        stream.next();
        // Symbol with string syntax
        ch = stream.peek();
        if (ch == '"') {
          stream.next();
          return chain(stream, state, tokenString('"', 'string'));
        }
        // Binary number
        else if (ch == 'b') {
          stream.next();
          stream.eatWhile(/[01]/);
          return 'number';
        }
        // Hex number
        else if (ch == 'x') {
          stream.next();
          stream.eatWhile(/[\da-f]/i);
          return 'number';
        }
        // Octal number
        else if (ch == 'o') {
          stream.next();
          stream.eatWhile(/[0-7]/);
          return 'number';
        }
        // Token concatenation in macros
        else if (ch == '#') {
          stream.next();
          return 'punctuation';
        }
        // Sequence literals
        else if (ch == '[' || ch == '(') {
          stream.next();
          return 'bracket';
          // Hash symbol
        } else if (stream.match(/f|t|all-keys|include|key|next|rest/i)) {
          return 'atom';
        } else {
          stream.eatWhile(/[-a-zA-Z]/);
          return 'error';
        }
      } else if (ch == '~') {
        stream.next();
        ch = stream.peek();
        if (ch == '=') {
          stream.next();
          ch = stream.peek();
          if (ch == '=') {
            stream.next();
            return 'operator';
          }
          return 'operator';
        }
        return 'operator';
      } else if (ch == ':') {
        stream.next();
        ch = stream.peek();
        if (ch == '=') {
          stream.next();
          return 'operator';
        } else if (ch == ':') {
          stream.next();
          return 'punctuation';
        }
      } else if ('[](){}'.indexOf(ch) != -1) {
        stream.next();
        return 'bracket';
      } else if ('.,'.indexOf(ch) != -1) {
        stream.next();
        return 'punctuation';
      } else if (stream.match('end')) {
        return 'keyword';
      }
      for (var name in patterns) {
        if (patterns.hasOwnProperty(name)) {
          var pattern = patterns[name];
          if (
            (pattern instanceof Array &&
              some(pattern, function(p) {
                return stream.match(p);
              })) ||
            stream.match(pattern)
          )
            return patternStyles[name];
        }
      }
      if (/[+\-*\/^=<>&|]/.test(ch)) {
        stream.next();
        return 'operator';
      }
      if (stream.match('define')) {
        return 'def';
      } else {
        stream.eatWhile(/[\w\-]/);
        // Keyword
        if (wordLookup.hasOwnProperty(stream.current())) {
          return styleLookup[stream.current()];
        } else if (stream.current().match(symbol)) {
          return 'variable';
        } else {
          stream.next();
          return 'variable-2';
        }
      }
    }

    function tokenComment(stream, state) {
      var maybeEnd = false,
        maybeNested = false,
        nestedCount = 0,
        ch;
      while ((ch = stream.next())) {
        if (ch == '/' && maybeEnd) {
          if (nestedCount > 0) {
            nestedCount--;
          } else {
            state.tokenize = tokenBase;
            break;
          }
        } else if (ch == '*' && maybeNested) {
          nestedCount++;
        }
        maybeEnd = ch == '*';
        maybeNested = ch == '/';
      }
      return 'comment';
    }

    function tokenString(quote, style) {
      return function(stream, state) {
        var escaped = false,
          next,
          end = false;
        while ((next = stream.next()) != null) {
          if (next == quote && !escaped) {
            end = true;
            break;
          }
          escaped = !escaped && next == '\\';
        }
        if (end || !escaped) {
          state.tokenize = tokenBase;
        }
        return style;
      };
    }

    // Interface
    return {
      startState: function() {
        return {
          tokenize: tokenBase,
          currentIndent: 0,
        };
      },
      token: function(stream, state) {
        if (stream.eatSpace()) return null;
        var style = state.tokenize(stream, state);
        return style;
      },
      blockCommentStart: '/*',
      blockCommentEnd: '*/',
    };
  });

  CodeMirror.defineMIME('text/x-dylan', 'dylan');
});
